/*
* Copyright 2006-2007 Javector Software LLC
*
* Licensed under the GNU General Public License, Version 2 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*      http://www.gnu.org/copyleft/gpl.html
*
* THE SOURCE CODE AND ACCOMPANYING FILES ARE PROVIDED WITHOUT ANY WARRANTY,
* WRITTEN OR IMPLIED.
*
* The copyright holder provides this software under other licenses for those
* wishing to include it with products or systems not licensed under the GPL.
* Contact licenses@javector.com for more information.
*/
package com.javector.soashopper.yahoo.api;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.namespace.QName;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.Dispatch;
import javax.xml.ws.Service;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.http.HTTPBinding;

import com.javector.soashopper.Category;
import com.javector.soashopper.yahoo.YahooShoppingCatalogListingClass;
import com.javector.soashopper.yahoo.YahooShoppingDepartment;
import com.javector.soashopper.yahoo.YahooShoppingSortStyle;
import com.javector.soashopper.yahoo.YahooShoppingTopLevelCategory;
import com.javector.util.Util;

import yahoo.merchoffers.CatalogListing;

import yahoo.prods.CatalogType;
import yahoo.prods.ProductSearch;
import yahoo.prods.ProductType;

/**
 * YahooRESTInterface is an implementation of the Yahoo Shopping REST APIs.
 * Unlike the eBay case, where a WSDL exists, there is no machine readable
 * interface description for Yahoo Shopping. Hence, we must write our own,
 * rather than using one generated by JAX-WS as is done in the eBay case.
 */
public class YahooRESTInterface {

  private static final String yahooShoppingNS = "http://api.shopping.yahoo.com/ShoppingService";

  private static final String productSearchURL = yahooShoppingNS
      + "/V2/productSearch";

  private static final String merchantsSearchURL = yahooShoppingNS
      + "/V1/merchantSearch";

  private static final String catalogListingsURL = yahooShoppingNS
      + "/V1/catalogListing";

  private static final String catalogSpecsURL = yahooShoppingNS
      + "/V1/catalogSpecs";

  private static final String userProductReviewURL = yahooShoppingNS
      + "/V1/userproductreview";

  // 6.0 on Windows XP SP2
  private static final String USER_AGENT = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)";

  private static final Hashtable<String, CatalogType> yahooCatalogCache = new Hashtable<String, CatalogType>();

  private Dispatch<Object> productSearchDispatch = null;

  private Dispatch<Object> merchantsSearchDispatch = null;

  private Dispatch<Object> catalogListingDispatch = null;

  private Dispatch<Object> catalogSpecsDispatch = null;

  private Dispatch<Object> userProductReviewSearchDispatch = null;

  private JAXBContext yahooShoppingContext = null;

  private String yahooShoppingAppId = null;

  /**
   * Configure the YahooShoppingService. To use this service, register with
   * Yahoo and get an application id. See
   * http://api.search.yahoo.com/webservices/register_application.
   * 
   * @param appid
   *          The application ID that you registered with the Yahoo Developer
   *          Network.
   */
  // ! <example xn="YahooRESTInterface_constructor">
  // ! <c>chap09</c><s>soapclient</s>
  public YahooRESTInterface(String appid) {

    yahooShoppingAppId = appid;
    // Create the JAXB context for Yahoo Shopping
    String contextPath = "yahoo.catalogspecs:yahoo.merchinfo:"
        + "yahoo.merchoffers:yahoo.prodrev:yahoo.prods";
    try {
      yahooShoppingContext = JAXBContext.newInstance(contextPath);
    } catch (JAXBException e) {
      throw new RuntimeException("Failed to create JAXBContext from: "
          + contextPath);
    }
    // Create ports for the 5 Yahoo Shopping services
    QName shoppingServiceQN = new QName(yahooShoppingNS, "ShoppingService");
    QName productSearchPortQN = new QName(yahooShoppingNS, "ProductSearch");
    QName merchantSearchQN = new QName(yahooShoppingNS, "MerchantSearch");
    QName catalogListingQN = new QName(yahooShoppingNS, "CatalogListing");
    QName catalogSpecsQN = new QName(yahooShoppingNS, "CatalogSpecs");
    QName userProductReviewQN = new QName(yahooShoppingNS,
        "UserProductReview");
    Service shoppingService = Service.create(shoppingServiceQN);
    shoppingService.addPort(productSearchPortQN, HTTPBinding.HTTP_BINDING,
        productSearchURL);
    shoppingService.addPort(merchantSearchQN, HTTPBinding.HTTP_BINDING,
        merchantsSearchURL);
    shoppingService.addPort(catalogListingQN, HTTPBinding.HTTP_BINDING,
        catalogListingsURL);
    shoppingService.addPort(catalogSpecsQN, HTTPBinding.HTTP_BINDING,
        catalogSpecsURL);
    shoppingService.addPort(userProductReviewQN, HTTPBinding.HTTP_BINDING,
        userProductReviewURL);
    // Create Dispatch instances for REST invocation
    productSearchDispatch = shoppingService.createDispatch(productSearchPortQN,
        yahooShoppingContext, Service.Mode.PAYLOAD);
    merchantsSearchDispatch = shoppingService.createDispatch(merchantSearchQN,
        yahooShoppingContext, Service.Mode.PAYLOAD);
    catalogListingDispatch = shoppingService.createDispatch(catalogListingQN,
        yahooShoppingContext, Service.Mode.PAYLOAD);
    catalogSpecsDispatch = shoppingService.createDispatch(catalogSpecsQN,
        yahooShoppingContext, Service.Mode.PAYLOAD);
    userProductReviewSearchDispatch = shoppingService.createDispatch(
        userProductReviewQN, yahooShoppingContext, Service.Mode.PAYLOAD);
    // Configure Dispatch object for GET method invocation
    productSearchDispatch.getRequestContext().put(
        MessageContext.HTTP_REQUEST_METHOD, "GET");
    merchantsSearchDispatch.getRequestContext().put(
        MessageContext.HTTP_REQUEST_METHOD, "GET");
    catalogListingDispatch.getRequestContext().put(
        MessageContext.HTTP_REQUEST_METHOD, "GET");
    catalogSpecsDispatch.getRequestContext().put(
        MessageContext.HTTP_REQUEST_METHOD, "GET");
    userProductReviewSearchDispatch.getRequestContext().put(
        MessageContext.HTTP_REQUEST_METHOD, "GET");

  }
  // ! </example>

  /**
   * The Yahoo Shopping Product Search (V2) API
   * {@link http://developer.yahoo.com/shopping/V2/productSearch.html} This is
   * the base level JAX-WS implementation of Yahoo Shopping product search that
   * returns a instance of the JAXB representation of the Yahoo Shopping
   * {urn:yahoo:prods}ProductSearch element.
   * 
   * @param keywords
   *          A list of space separated keywords to search for. Required if
   *          <code>category</code> is not specified.
   * @param category
   *          The Top Level Category to be searched. Required if
   *          <code>keywords</code> is not specified.
   * @param catClass
   *          Limits response to catalog listings (catalogs), free merchant
   *          listings (freeoffers), or paid merchant listings (paidoffers). You
   *          may also use the comma-separated list catalogs,paidoffers to
   *          return a combination of catalog and paidoffer listings. Not
   *          required.
   * @param dept
   *          Only used if the category parameter is NOT specified. The specific
   *          department of Yahoo! Shopping to search.
   * @param highestPrice
   *          The maximum price for returned items (in US dollars).
   * @param lowestPrice
   *          The minimum price for returned items (in US dollars).
   * @param maxRefines
   *          Only used if category is specified. Specifies the maximum number
   *          of refinement values, per refinement key, to be returned by the
   *          request. Must be between 0-2000. Default is 10.
   * @param merchantId
   *          If specified, the service will only return results offered by this
   *          merchant. The argument will be ignored if a non-numeric category
   *          is specified.
   * @param refinements
   *          Only used if category is specified. Refinement keys. Multiple
   *          refinement keys can be specified e.g.
   *          &refinement=4354-Brand=Dell&refinement=1234-Store=CompUsa
   * @param results
   *          The number of results to return. Must be between 1-50. Default is
   *          10 if null is specified.
   * @param showNumRatings
   *          When set to 1 (true), returns the number of ratings for each
   *          product (if available). Caution: If set to 1 (true), it will
   *          result in increased response time for your request.
   * @param showSubCategories
   *          When set to 1 (true), returns subcategories (if any).
   * @param sort
   *          Sort by price or user rating. If no sort is specified, results are
   *          sorted by relevancy. Note: Sorting by user rating,
   *          userrating_ascending or userrating_descending, will not have any
   *          effect unless a non-numeric category parameter is used.
   * @param start
   *          The starting result position to return. The starting position
   *          cannot exceed 300.
   * @return a top level container including categories, refinements, and
   *         products (offers and catalog elements).
   */
  // ! <example xn="YahooRESTInterface_productSearch">
  // ! <c>chap09</c><s>restclient</s>
  public ProductSearch productSearch(String keywords,
      YahooShoppingTopLevelCategory category,
      YahooShoppingCatalogListingClass catClass, YahooShoppingDepartment dept,
      Double highestPrice, Double lowestPrice, Integer maxRefines,
      String merchantId, Map<String, String> refinements, Integer results,
      Boolean showNumRatings, Boolean showSubCategories,
      YahooShoppingSortStyle sort, Integer start) {

    if (keywords == null && category == null) {
      throw new IllegalArgumentException(
          "Both keywords and category cannot be null.");
    }
    String query = "appid=" + yahooShoppingAppId;
    // query
    if (keywords != null) {
      query += "&query=" + keywords.replace(" ", "%20");
    }
    // category
    if (category != null) {
      query += "&category=" + category.getCategoryId();
    }
    // highestprice
    Util util = new Util();
    if (highestPrice != null) {
      query += "&highestprice=" + util.floor(highestPrice, 2);
    }
    // lowestprice
    if (lowestPrice != null) {
      query += "&lowestprice=" + util.ceiling(lowestPrice, 2);
    }
    // ! </example>
    // class
    if (catClass != null
        && !catClass.equals(YahooShoppingCatalogListingClass.ALL)) {
      query += "&class=" + catClass.getValue();
    }
    // department
    if (category != null && dept != null) {
      throw new IllegalArgumentException(
          "The dept parameter should be non-null only if category is null.");
    }
    if (dept != null) {
      query += "&department=" + dept.getCode();
    }
    // max_refines
    if (maxRefines != null && category == null) {
      throw new IllegalArgumentException(
          "The maxRefines parameter can only be used together with a category.");
    }
    if (maxRefines != null) {
      if (maxRefines.intValue() < 0 || maxRefines.intValue() > 2000) {
        throw new IllegalArgumentException(
            "The maxRefines parameter must be between 0-2000.");
      }
      query += "&max_refines=" + maxRefines.intValue();
    }
    // merchantid
    if (merchantId != null) {
      query += "&merchantid=" + merchantId;
    }
    // refinement
    if (refinements != null && category == null) {
      throw new IllegalArgumentException(
          "Refinements can only be specified when the category parameter is not null.");
    }
    if (refinements != null) {
      for (String key : refinements.keySet()) {
        String val = refinements.get(key);
        query += "&refinement=" + key + "=" + val;
      }
    }
    // results
    if (results != null) {
      if (results.intValue() < 1 || results.intValue() > 50) {
        throw new IllegalArgumentException(
            "results parameter must be between 1-50");
      }
      query += "&results=" + results.intValue();
    }
    // show_numratings
    if (showNumRatings != null) {
      if (showNumRatings.booleanValue()) {
        query += "&show_numratings=1";
      } else {
        query += "&show_numratings=0";
      }
    }
    // show_subcategories
    if (showSubCategories != null) {
      if (showSubCategories.booleanValue()) {
        query += "&show_subcategories=1";
      } else {
        query += "&show_subcategories=0";
      }
    }
    // sort
    if (sort != null) {
      query += "&sort=" + sort.getValue();
    }
    if (start != null) {
      if (start.intValue() < 1 || start.intValue() > 300) {
        throw new IllegalArgumentException(
            "start parameter must be between 1 - 300.");
      }
      query += "&start=" + start.intValue();
    }
    
    // ! <example xn="YahooRESTInterface_invocation">
    // ! <c>chap09</c><s>restclient</s>
    productSearchDispatch.getRequestContext().put(MessageContext.QUERY_STRING,
        query);
    setRequestHeaders(productSearchDispatch.getRequestContext());
    ProductSearch searchResults = null;
    try {
      String urlStr = (String)
        productSearchDispatch.getRequestContext().get(BindingProvider.ENDPOINT_ADDRESS_PROPERTY);
      System.out.println("Dispatching to URL: " + urlStr);
      searchResults = (ProductSearch) productSearchDispatch.invoke(null);
    } catch (Exception e) {
      throw new RuntimeException("YahooShopping Product Search: " + 
          productSearchURL + "?" + query + " threw an Exception", e);
    }
    // ! </example>
    /*
     * Load the productId/CatalogType into the cache as Yahoo does not provide a
     * mechanism to retrieve the CatalogType again from the API.
     */
    loadCatalogTypesToCache(searchResults);
    return searchResults;

  }

  /**
   * The Yahoo Shopping Catalog Listing (V1) API. This Catalog Listing service
   * allows you to display price information for offers aggregated as Buyer's
   * Guides by Yahoo! Shopping, in particular those identified by a Catalog ID
   * via the Product Search service. This is the base level JAX-WS
   * implementation of Yahoo Shopping catalog listing that returns a instance of
   * the JAXB representation of the Yahoo Shopping
   * {urn:yahoo:merchoffers}CatalogListing element. See
   * {@link http://developer.yahoo.com/shopping/V1/catalogListing.html}
   * 
   * @param catalogid
   *          The ID of the Yahoo! Shopping Buyers Guide catalog, as derived
   *          from a Catalog in a Product Search. Required. May not be null.
   * @param upc
   *          The UPC code of the product to search for. This can be useful for
   *          disambiguating books/CDs/DVDs, etc. May be <code>null</code>.
   * @param zip
   *          The zip code being ordered from (for calculating shipping and
   *          taxes if possible). May be <code>null</code>.
   * @param onlynew
   *          If <code>false</code>, the search will also display used or
   *          refurbished items along with new items. When <code>true</code>,
   *          only new item are displayed.
   * @param start
   *          The starting result position to return (1-based). The number of
   *          the finishing position is (start + results - 1). If
   *          <code>null</code>, then the defaul value of 1 is used.
   * @param results
   *          The number of results to return. If <code>null</code> then all
   *          the results are returned.
   * @return The individual product offerings corresponding to the catalog
   *         listing.
   */
  public CatalogListing getCatalogListing(String catalogid, String upc,
      String zip, Boolean onlynew, Integer start, Integer results) {

    if (catalogid == null) {
      throw new IllegalArgumentException("Catalog ID cannot be null.");
    }
    String query = "appid=" + yahooShoppingAppId + "&catalogid=" + catalogid;
    if (upc != null && !upc.equals("")) {
      query += "&upc=" + upc;
    }
    if ((new Util()).validZip(zip)) {
      query += "&zip=" + zip;
    }
    if (onlynew != null) {
      if (onlynew.booleanValue()) {
        query += "&onlynew=1";
      } else {
        query += "&onlynew=0";
      }
    }
    if (start != null) {
      query += "&start=" + start.toString();
    }
    if (results != null) {
      query += "&results=" + results.toString();
    }
    catalogListingDispatch.getRequestContext().put(MessageContext.QUERY_STRING, query);
    setRequestHeaders(catalogListingDispatch.getRequestContext());
    CatalogListing catalogListing = null;
    try {
      catalogListing = (CatalogListing) catalogListingDispatch.invoke(null);
    } catch (Exception e) {
      throw new RuntimeException(
          "catalogListingDispatch.invoke(null) threw an Exception", e);
    }
    return catalogListing;

  }

  public static CatalogType getCatalogTypeFromCache(String productId) {
    return yahooCatalogCache.get(productId);
  }

  public static void addCatalogTypeToCache(String productId, CatalogType product) {
    yahooCatalogCache.put(productId, product);
  }

  public static void loadCatalogTypesToCache(ProductSearch productSearchResults) {
    List<ProductType> productList = productSearchResults.getProducts()
        .getProduct();
    for (ProductType p : productList) {
      if (p.getCatalog() != null) {
        String id = p.getCatalog().getId().toString();
        addCatalogTypeToCache(id, p.getCatalog());
      }
    }
  }

  private void setRequestHeaders(Map<String, Object> requestContext) {
    
    Map requestHeaders = (Map) requestContext
        .get(MessageContext.HTTP_REQUEST_HEADERS);
    if (requestHeaders == null) {
      requestHeaders = new HashMap<String, String>();
      requestContext.put(MessageContext.HTTP_REQUEST_HEADERS, requestHeaders);
    }
    List<String> userAgentList = new ArrayList<String>();
    userAgentList.add(USER_AGENT);
    requestHeaders.put("User-agent", userAgentList);
    
  }
  
}
